Skip to content

πŸ› [v6] contributor-first: a relay at its task limit does not cover a lane - #17268

Merged
hivecommons-hive[bot] merged 2 commits into
hivecommons:v6from
Danathar:fix/v6-limit-reached-relay-not-cover
Nov 8, 2026
Merged

hivecommons-hive[bot] merged 2 commits into
hivecommons:v6from
Danathar:fix/v6-limit-reached-relay-not-cover

Conversation

@Danathar

@Danathar Danathar commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Summary

A contributor relay that has used up its hourly or daily task allowance still counted as covering its contributor-first lane. The hub refuses that relay work until the window rolls, so the governor kept the resident agent's kicks withheld while nobody worked the lane: up to an hour for the hourly cap, up to most of a day for the daily one. Coverage now leaves such a relay out. It uses the same limit check that task selection refuses work with, now factored into one function both call, so the two can't drift apart.

This is the same class of bug as the standby and quota-hold gaps that #17233 handled in RoleCoverage. Refs #17233.

What went wrong

RoleCoverage (src/pkg/dashboard/contribute_coverage.go) counts a relay as cover when its role, heartbeat, failure-streak, standby and quota-hold state say it is taking work. It never looked at the per-tier rate limits. Task selection (selectTaskPass in contribute_select.go) refuses a relay with hourly_limit or daily_limit once its identity has been handed hub.tier_limits.<tier>.max_per_hour tasks in the last hour or max_per_day in the last 24 hours. So a relay at, say, 10 of 10 for the day was refused every ready, but still reported as covering, and the resident agent stayed idle until the window rolled.

What changes

  • One shared limit check. New tierRateLimitReached(identity, tier, now) in contribute_select.go holds the hourly/daily rule that used to be inline in selectTaskPass. It returns which cap was hit plus the count and the cap. Selection now calls it and keeps its exact log lines, decision text and refusal reasons.
  • Coverage uses it. RoleCoverage skips a candidate whose identity is at either cap. The check runs after h.mu is released, next to the failure-streak check, so no lock is nested inside h.mu.
  • Docs. The RoleCoverage doc comment, the governor's contributor-first header comment, and the "Contributor-first lanes" list in src/docs/agent-configuration.md now name the rule. They also say that a relay holding its max_concurrent tasks is busy and still counts.

How selection gets the limits, which the shared function keeps as-is: hub.tier_limits[<trust tier>] and nothing else. Config load fills in the shipped row for each built-in tier the operator didn't set (config/defaults.go). There is no per-contributor override (ContributorProfile.RateLimits is stored but never enforced). A tier with no row, or a cap of 0, is unlimited. The hourly cap is checked first.

Locking and cost. The count comes from rateWindowCounts, which takes only rateMu. That lock is never held across h.mu, and coverage never takes selectMu. It scans at most one day of assignment stamps for one identity, once per candidate relay per governor evaluation. RoleCoverage is called once per eval cycle (cmd/hive/main_eval_cycle.go). The config is read unlocked, the same way selection and contributionsSuspended read it.

Files

  • src/pkg/dashboard/contribute_select.go: new tierRateLimitHit / tierRateLimitReached; selection calls it.
  • src/pkg/dashboard/contribute_coverage.go: RoleCoverage skips a relay at its cap; doc comment.
  • src/pkg/dashboard/contribute_coverage_test.go: new tests.
  • src/pkg/governor/contributor_first.go: header comment only, no gate logic.
  • src/docs/agent-configuration.md: contributor-first coverage list.
  • changelog.d/fixed-limit-reached-relay-not-cover.md

Tests

New, in contribute_coverage_test.go:

  • TestRoleCoverage_HourlyLimitReachedDoesNotCover: at the hourly cap, real selection refuses with hourly_limit and the relay doesn't cover.
  • TestRoleCoverage_DailyLimitReachedDoesNotCover: at the daily cap with nothing in the last hour, selection refuses with daily_limit and the relay doesn't cover.
  • TestRoleCoverage_UnderRateLimitsCovers: a relay one short of both caps still covers, and a stamp older than 24h doesn't count. A same-tier relay at the hourly cap in the same lane is the only one dropped.
  • TestTierRateLimitReached: table test of the shared rule (under, hourly, daily, both report hourly, rolled-off stamps, zero caps, a tier with no row, no server, nil hub).

On v6 tip fd10c75e5 the three RoleCoverage tests fail: the at-cap relay is reported as cover. Their setup checks, which ask real selection and see it refuse, pass on the tip, which shows the mismatch directly. TestTierRateLimitReached doesn't build on the tip because the function is new. All four pass with the change, along with the existing TestRateLimits_* selection tests.

Ran in full: go test ./pkg/dashboard/... ./pkg/governor/... and go vet on both. Docs citation, docs link and api-reference citation checks pass.

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1

…lane

RoleCoverage counted a relay as covering its role even when task
selection refused it work because its identity had used up its tier's
max_per_hour or max_per_day. The relay stays connected and is refused
every ready with hourly_limit / daily_limit until the window rolls, so
the governor kept the resident agent's kicks withheld while nobody
worked the lane. Same class as the standby / quota-hold gaps hivecommons#17233
handled.

- Factor the hourly/daily rule out of selectTaskPass into
  tierRateLimitReached (hub.tier_limits[tier], caps <= 0 unlimited,
  hourly checked first). Selection keeps its log lines, decision text
  and refusal reasons.
- RoleCoverage skips a candidate whose identity is at either cap, using
  the same function, after h.mu is released (rateMu only; no selectMu).
- Docs: RoleCoverage comment, governor contributor-first header, and
  the contributor-first coverage list in agent-configuration.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Danathar <Danathar@users.noreply.github.com>
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign nicholasjackson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Open PR overlap check

Base branch: v6

Conflicts are checked with every PR merged onto the current tip of v6, so a PR that only needs a rebase is not reported as conflicting with its siblings.

PR Title Relation Files
#17265 🌱 Forward-merge v5 into v6 (100 commits) same files src/docs/agent-configuration.md

@hivecommons-hive
hivecommons-hive Bot merged commit af42053 into hivecommons:v6 Nov 8, 2026
69 of 70 checks passed
@hivecommons-hive

hivecommons-hive Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Hive status β€” updated in place; edits do not notify. Full history is in the Activity Monitor.

Evidence bundle `#172684cc43be` sha256:932b66842d609ce91ab4813df501a1dca6019067726b9884bbe19c0c58d1be57 β€” download from the dashboard or `hivectl review evidence #17268`.

Reviewed by reviewer β€” no findings.

Reviewer notes

Read the tree at merge-base 74620f6 and the PR head 4cc43be, not just the diff. This looks correct to me.

Behaviour preserved in selection. The old inline hourly/daily block in selectTaskPass ran only when h.server.deps != nil && Config != nil and TierLimits[tier] had a row; tierRateLimitReached (src/pkg/dashboard/contribute_select.go:189-206) re-implements exactly those guards (plus a nil-h/nil-server guard), the same <= 0 = unlimited rule, and the same hourly-before-daily order. Log lines, recordDecision text and taskUnavailable reasons are unchanged (contribute_select.go:634-651). Concurrency stays inside the original guarded block, which is fine since the rate check no longer depends on it.

Coverage uses the same inputs. RoleCoverage takes tier from c.profile.TrustTier under c.mu (contribute_coverage.go:68-71), which is the same source selection uses (contribute_select.go:550-552), so a relay with no profile or empty tier is treated identically on both paths. The check runs after h.mu.RUnlock() next to contributorFailureStreakActive, and rateWindowCounts only takes rateMu (contribute_select.go:140-141), so no new lock nesting is introduced. The one side effect β€” rateWindowCounts prunes stamps older than the day window β€” is benign; those stamps could never count again.

Tests cover hourly, daily, under-limit (including a rolled-off 25h stamp and a mixed-tier lane) and the shared function directly, and selectionRefusal asserts selection and coverage agree. Docs (src/docs/agent-configuration.md, governor header comment) and the changelog fragment match the behaviour.

No findings.

@hivecommons-hive

Copy link
Copy Markdown
Contributor

merged by Hive trusted-author auto-merge: author @Danathar holds role owner and GitHub permission maintain on repo; CI green at 4cc43be.

hivecommons-hive Bot added a commit that referenced this pull request Nov 8, 2026
…ing work does not cover a lane (#17284)

RoleCoverage counted a relay as covering its role when the hub would not
hand it work (revoked, or its trust tier in hub.disabled_tiers) and when
every ready it sent was answered no_matching_work. Both stay connected,
so the governor kept the resident agent's kicks withheld while nobody
worked the lane. Seen on hive-wild-mole on 2026-11-08: a relay covered
scanner while selection answered no_matching_work every 30 s. Same class
as #17233 and #17268.

- Factor revoked / disabled tier into tierAccessGateReason (disabled half:
  tierDisabled). resumeGateReason keeps its reasons and order; fresh
  selection's disabled_tiers loop now calls tierDisabled. RoleCoverage
  reads the tier under c.mu and evaluates with h.mu released.
- Record the idle run on the connection: lastIdleReasonSince beside
  lastIdleReason, set on task_unavailable in the ready and yank paths,
  no_matching_work and repo_unmintable folded into one run. Cleared on the
  selection commit, standby dispatch, resume adoption and sendFixTask.
- RoleCoverage skips a relay holding no task that has been answered
  no_matching_work / repo_unmintable for coverageNoWorkWindow (2 min from
  the first answer; the relay retries every 30 s and the governor still
  applies contributor_first_grace on top).
- Docs: RoleCoverage comment, agent-configuration.md coverage list,
  governor header, one drifted citation; changelog fragment.


Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1

Signed-off-by: Danathar <Danathar@users.noreply.github.com>
Co-authored-by: Danathar <Danathar@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: hivecommons-hive[bot] <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant